

import com.google.common.base.Function;
import com.google.common.collect.Collections2;

import java.util.*;


public class Puzzle {
    private final Cell[] cells;
    private final int rows;
    private final int cols;
    private final List<Shape> shapes = new ArrayList<Shape>();
    private final Map<Character, Shape> idToShapeMap = new HashMap<Character, Shape>();
    private final int numberOfWalls;

    
    //read in puzzle diagram 
    
    public Puzzle(String puzzle) {
        final List<String> rowStrings = Arrays.asList(puzzle.split("\\r|\\r\\n|\\n"));

        this.rows  = rowStrings.size();
        this.cols  = findMaxRowLength(rowStrings);
        this.cells = new Cell[this.rows*this.cols]; //create puzzle tray  with x rows, y columns
        this.resetCells();

        int wallsFound = 0;
        for (int row = 0; row < this.rows; row++) {
            final String rowString = rowStrings.get(row);
            
            for (int col = 0; col < this.cols; col++) {
                if (col >= rowString.length()) { continue; }

                final char cellCharacter = rowString.charAt(col);
                final Cell cell          = this.getCell(row, col);

                //parse # character in input file as wall
                switch (cellCharacter) {
                    case ' ':
                        break;
                    case '#':
                        cell.isWall = true;
                        wallsFound++;
                        break;
                    default:
                        this.assignShapeToCellById(cellCharacter, cell);
                }
            }
        }
        
        this.numberOfWalls = wallsFound;
    }

    private static int findMaxRowLength(List<String> rowStrings) {
        return Collections.max(Collections2.transform(rowStrings, new Function<String, Integer>() {
            public Integer apply(String input) {
                return  input.length();
            }
        }));
    }
    
    public Puzzle(Puzzle puzzle) {
        this.rows = puzzle.rows;
        this.cols = puzzle.cols;
        this.cells = new Cell[rows*cols];
        this.numberOfWalls = puzzle.numberOfWalls;

        int i = 0;
        for (short row = 0; row < this.rows; row++) {
            for (short col = 0; col < this.cols; col++, i++) {
                final Cell puzzleCell = puzzle.getCell(row, col);

                if (puzzleCell.isWall()) {
                    this.cells[i] = puzzleCell;
                    continue;
                }

                final Cell goalCell = new Cell();
                goalCell.row        = row;
                goalCell.col        = col;

                if (puzzleCell.shape != null) {
                    this.assignShapeToCellById(puzzleCell.shape.getId(), goalCell);
                }

                this.cells[i] = goalCell;
            }
        }
    }

    public String createEquivalentStateString(Map<Character, Character> idMap) {
        final char[] result = new char[this.cols*this.rows - this.numberOfWalls];

        int i = 0;
        for (int row = 0; row < this.rows; row++) {
            for (int col = 0; col < this.cols; col++) {
                final Cell cell = this.getCell(row, col);
                
                if (cell.isWall) {continue;}
                final Character target    = cell.toChar();
                final Character transform = idMap.get(target);

                result[i++] = (transform == null) ? target : transform;
            }
        }
        
        return new String(result);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        for (int row = 0; row < this.rows; row++) {
            for (int col = 0; col < this.cols; col++) {
                sb.append(this.getCell(row, col).toString());
            }
            if (row < this.rows - 1) { sb.append(String.format("%n")); }
        }

        return sb.toString();
    }

    //create blank tray
    private void resetCells() {
        for (short row = 0; row < this.rows; row++) {
            for (short col = 0; col < this.cols; col++) {
                final Cell cell = new Cell();
                cell.isWall = false;
                cell.row    = row;
                cell.col    = col;
                cell.shape  = null;

                this.cells[(this.cols*row) + col] = cell;
            }
        }
    }

    //get the shape of the piece given an ID such
    // as A, B, C....
    private Shape getShapeForId(char id) {
        Shape shape = this.idToShapeMap.get(id);
        
        if (shape != null) { return shape; }

        final Shape newShape = new Shape(id);
        this.shapes.add(newShape);
        this.idToShapeMap.put(id, newShape);
        return newShape;
    }

    private void assignShapeToCellById(char cellCharacter, Cell cell) {
        final Shape shape = this.getShapeForId(cellCharacter);

        this.assignShapeToCell(shape, cell);
    }

    private void assignShapeToCell(Shape shape, Cell cell) {
        cell.shape = shape;
        shape.addCell(cell);
    }
    
    public Cell getCell(int row, int col) {
        if (row < 0 || row >= this.rows || col < 0 || col >= this.cols) {
            throw new IllegalArgumentException("Invalid row and col for this puzzle: " + row + "," + col);
        }

        final int index = (this.cols*row) + col;
        return this.cells[index];
    }

    public List<Shape> getShapes() {
        return Collections.unmodifiableList(shapes);
    }

    public int getCols() {
        return cols;
    }

    public int getRows() {
        return rows;
    }

    public void applyMove(Move move) {
        final Shape shape = move.getShape();
        final List<Cell> cellsBeforeMove = new ArrayList<Cell>(shape.getCells());

        removeShape(shape);
        
        for (Cell oldCell : cellsBeforeMove) {
            switch (move.getDirection()) {
                case UP:
                    this.assignShapeToCell(shape, this.getCell(oldCell.row - 1, oldCell.col));
                    break;
                case RIGHT:
                    this.assignShapeToCell(shape, this.getCell(oldCell.row, oldCell.col + 1));
                    break;
                case DOWN:
                    this.assignShapeToCell(shape, this.getCell(oldCell.row + 1, oldCell.col));
                    break;
                case LEFT:
                    this.assignShapeToCell(shape, this.getCell(oldCell.row, oldCell.col - 1));
                    break;
            }
        }
    }

    public void unapplyMove(Move move) {
        final Move.Direction direction = move.getDirection();
        
        // check if move direction exists (check for nulls)
        final Move.Direction reverseDirection = (direction == Move.Direction.UP) ? Move.Direction.DOWN :
                                                (direction == Move.Direction.RIGHT) ? Move.Direction.LEFT :
                                                (direction == Move.Direction.DOWN) ? Move.Direction.UP :
                                                Move.Direction.RIGHT;
        this.applyMove(new Move(move.getShape(), reverseDirection));
    }


    private void removeShape(Shape shape) {
        shape.clear();
    }


    public static class Cell {
        private boolean isWall;
        private short   row;
        private short   col;
        private Shape   shape;

        public Shape getShape() {
            return shape;
        }

        public int getRow() {
            return row;
        }

        public int getCol() {
            return col;
        }

        public boolean isWall() {
            return isWall;
        }

        public char toChar() {
            return (isWall()) ? '#' : (shape == null) ? ' '  : shape.getId();
        }

        @Override
        public String toString() {
            return Character.toString(this.toChar());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Cell cell = (Cell) o;

            if (col != cell.col) return false;
            if (isWall != cell.isWall) return false;
            if (row != cell.row) return false;
            return !(shape != null ? !shape.equals(cell.shape) : cell.shape != null);

        }

        @Override
        public int hashCode() {
            int result = (isWall ? 1 : 0);
            result = 31 * result + row;
            result = 31 * result + col;
            result = 31 * result + (shape != null ? shape.hashCode() : 0);
            return result;
        }

        public boolean isEmpty() {
            return !isWall && shape == null;
        }

        public void removeShapeReference() {
            this.shape = null;
        }


    }

}
